/*
 * Copyright 2010 Yannick Stucki (yannickstucki.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.yannickstucki.android.musicqueue.ui;

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import com.yannickstucki.android.musicqueue.communication.OnDragListener;
import com.yannickstucki.android.musicqueue.util.Logger;

/**
 * A view which shows a list of items that can be dragged around to change order
 * or swiped out to delete them. The view has a fixed size of items that scale
 * in height to fit to the hight given to this view.
 * 
 * @author Yannick Stucki (yannickstucki@gmail.com)
 * 
 * @param <T>
 *          This view can hold objects of any arbitrary class T given a
 *          viewGenerator class that transforms any instance of T to and
 *          instance of View
 */
public class DragView<T> extends ViewGroup {

  /**
   * The number of items displayed in this view. Same as items.length and
   * content.length.
   */
  private int count = 0;

  /**
   * The displayed items which are the generated views from the instances of T
   * stored in {@link #content}. This array is changed on the fly as the user
   * drags and swipes the items.
   */
  private View[] items;

  /**
   * The content i.e. data representation behind the displayed items. This array
   * is only updated once the dragging of items is finished or after a swipe.
   */
  private T[] content;

  /**
   * The height of the displayed items is calculated in a way that the whole
   * screen is filled. itemHeight * count == the height of the whole screen.
   */
  private int itemHeight = 0;

  /**
   * The start position of a drag (as an index of items). This is where the user
   * puts his finger down and this index points to the item in {@link #content}
   * which was being dragged.
   */
  private int startPosition;

  /**
   * The last position of the current drag (as an index of items). Used to
   * determine whether the user is dragging an item down or up.
   */
  private int lastPosition = -1;

  /**
   * The relative offset in x and y coordinates the dragged item has to where
   * the user puts down his finger. This way the item doesn't "jump" on the
   * screen once the user puts down his finger, but is always floating at an
   * offset.
   */
  private int xOffset, yOffset;

  /**
   * These dimensions are important for layouting the items. "Bottom" is not
   * needed, since this is implicitly stored in {@link #itemHeight}.
   */
  private int left, top, right;

  /**
   * Decides how far the delta of the x coordinate has to be on a swipe that it
   * counts as removing the item.
   */
  private int removeThreshold;

  /**
   * Remove threshold reached if at least 1/constant of the width of the screen
   * have been swiped.
   */
  private static final int REMOVE_THRESHOLD_CONSTANT = 3;

  /**
   * An interface which takes an instance of object T and returns an instance of
   * View.
   */
  private IViewGenerator<T> viewGenerator;

  /**
   * Listens to remove and completed drag events.
   */
  private OnDragListener<T> listener;

  /**
   * TODO: not sure this invisibility count is the optimal solution
   * adjust later for allowing asynchron pushing of songs.
   */
  private int invisibilityCount = 0;

  /**
   * Constructor for inflating by XML.
   */
  public DragView(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  /**
   * Constructor allowing defStyle.
   */
  public DragView(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
  }

  /**
   * Constructor for creating in code.
   * 
   * @param context
   */
  public DragView(Context context) {
    super(context);
  }

  /**
   * Sets the onDragListener.
   */
  public void setOnDragListener(OnDragListener<T> listener) {
    this.listener = listener;
  }

  /**
   * Sets the viewGenerator. This has to be called before
   * {@link #setContent(Object[])}.
   */
  public void setViewGenerator(IViewGenerator<T> viewGenerator) {
    this.viewGenerator = viewGenerator;
  }

  /**
   * Sets the contents of this view. Important, call setViewGenerator first.
   */
  public void setContent(T[] content) {
    this.content = content;
    cleanUpItems();
    count = content.length;
    items = new View[count];
    for (int i = 0; i < count; i++) {
      items[i] = viewGenerator.getView(null, content[i]);
      addView(items[i]);
    }
  }

  /**
   * Notify that the content has changed.
   */
  public void notifyContentChanged() {
    for (int i = 0; i < count; i++) {
      viewGenerator.getView(items[i], content[i]); // TODO: can we assume that
                                                   // the convertView is used?
    }
  }

  /**
   * Removes possibly attached items.
   */
  private void cleanUpItems() {
    if (items != null) {
      for (int i = 0; i < count; i++) {
        removeView(items[i]);
      }
    }
  }

  /**
   * Performs the move operation on the content. This is called after dragging
   * an item is completed to also let the content change. The listener is
   * notified with the new content.
   */
  private void move(int i, int j) {
    T temp = content[i];
    if (i < j) {
      for (int k = i; k < j; k++) {
        content[k] = content[k + 1];
      }
    } else {
      for (int k = i; k > j; k--) {
        content[k] = content[k - 1];
      }
    }
    content[j] = temp;
    if ((i == 0 || j == 0) && listener != null) {
      listener.onTopElementChanged();
    }
  }

  /**
   * called when a remove swipe has been performed. Both, items and content
   * needs to be updated. The listener is notified with the index of the removed
   * item.
   * 
   * It is in the listeners responsibility to get a new element to put into this
   * view ASAP do this by calling {@link #pushElement(Object)}.
   * 
   * TODO: have a better solution instead of just making the last elements
   * invisible. Make sure users can't drag down there anymore.
   */
  private void remove(int i) {
    Logger.debug("remove");
    View temp = items[i];
    for (int j = i; j < count - 1; j++) {
      content[j] = content[j + 1];
      items[j] = items[j + 1];
      items[j].layout(left, top + j * itemHeight, right, top + (j + 1) * itemHeight);
    }
    items[count - 1] = temp;
    temp.setVisibility(INVISIBLE);
    invisibilityCount++;
    content[count - 1] = null;
    if (listener != null) {
      if (i == 0) {
        listener.onTopElementChanged();
      }
      listener.onElementRemoved();
    }
  }

  /**
   * Pushes the next element to be displayed in this view.
   */
  public void notifyElementPushed() {
    if (invisibilityCount == 0) {
      invisibilityCount = 1;
    }
    int c = count - invisibilityCount;
    viewGenerator.getView(items[c], content[c]); // TODO: assume recycling?
    items[c].setVisibility(VISIBLE);
    invisibilityCount--;
  }


  @Override
  public boolean onTouchEvent(MotionEvent event) {
    int position = (int) (event.getY() - top) / itemHeight;
    if (position < 0 || position >= count) {
      return false;
    }

    switch (event.getAction()) {
    case MotionEvent.ACTION_DOWN:
      startPosition = position;
      lastPosition = position;
      yOffset = (int) (event.getY() % itemHeight);
      xOffset = (int) event.getX();
      break;
    case MotionEvent.ACTION_MOVE:
      int dragX = (int) event.getX() - xOffset;
      int dragY = (int) event.getY() - yOffset;

      if (lastPosition != position) {
        View temp = items[lastPosition];
        items[lastPosition] = items[position];
        items[position] = temp;

        items[lastPosition].layout(left, top + lastPosition * itemHeight, right, top
            + (lastPosition + 1) * itemHeight);
      }

      items[position].layout(dragX, dragY, dragX + right, dragY + itemHeight);
      lastPosition = position;
      break;
    case MotionEvent.ACTION_UP:
      if (position != startPosition) {
        move(startPosition, position);
        items[position].layout(left, top + position * itemHeight, right, top + (position + 1)
            * itemHeight);
      } else if (Math.abs(xOffset - event.getX()) > removeThreshold) {
        remove(position);
      } else {
        items[position].layout(left, top + position * itemHeight, right, top + (position + 1)
            * itemHeight);
      }
      break;
    default:
      break;
    }

    return true;
  }

  /**
   * We always need the size given from above.
   */
  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measureChildren(widthMeasureSpec, heightMeasureSpec);
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  }

  /**
   * We layout all the children in to the full width and give them all an equal
   * fraction of the total height.
   */
  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    left = l;
    top = t;
    right = r;
    removeThreshold = (right - left) / REMOVE_THRESHOLD_CONSTANT;
    if (count != 0) {
      itemHeight = (b - t) / count;
      for (int i = 0; i < count; i++) {
        items[i].layout(l, t + i * itemHeight, r, t + (i + 1) * itemHeight);
      }
    }
  }
}
